/*
 * Copyright (C) 2023 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.gson.it;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.function.BiConsumer;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;

/**
 * Integration test verifying behavior of shrunken and obfuscated JARs.
 */
@RunWith(Parameterized.class)
public class ShrinkingIT {
  // These JAR files are prepared by the Maven build
  public static final Path PROGUARD_RESULT_PATH = Paths.get("target/proguard-output.jar");
  public static final Path R8_RESULT_PATH = Paths.get("target/r8-output.jar");

  @Parameters(name = "{index}: {0}")
  public static List<Path> jarsToTest() {
    return Arrays.asList(PROGUARD_RESULT_PATH, R8_RESULT_PATH);
  }

  @Parameter
  public Path jarToTest;

  @Before
  public void verifyJarExists() {
    if (!Files.isRegularFile(jarToTest)) {
      fail("JAR file " + jarToTest + " does not exist; run this test with `mvn clean verify`");
    }
  }

  @FunctionalInterface
  interface TestAction {
    void run(Class<?> c) throws Exception;
  }

  private void runTest(String className, TestAction testAction) throws Exception {
    // Use bootstrap class loader; load all custom classes from JAR and not
    // from dependencies of this test
    ClassLoader classLoader = null;

    // Load the shrunken and obfuscated JARs with a separate class loader, then load
    // the main test class from it and let the test action invoke its test methods
    try (URLClassLoader loader = new URLClassLoader(new URL[] {jarToTest.toUri().toURL()}, classLoader)) {
      Class<?> c = loader.loadClass(className);
      testAction.run(c);
    }
  }

  @Test
  public void test() throws Exception {
    StringBuilder output = new StringBuilder();

    runTest("com.example.Main", c -> {
      Method m = c.getMethod("runTests", BiConsumer.class);
      m.invoke(null, (BiConsumer<String, String>) (name, content) -> output.append(name + "\n" + content + "\n===\n"));
    });

    assertThat(output.toString()).isEqualTo(String.join("\n",
        "Write: TypeToken anonymous",
        "[",
        "  {",
        "    \"custom\": 1",
        "  }",
        "]",
        "===",
        "Read: TypeToken anonymous",
        "[ClassWithAdapter[3]]",
        "===",
        "Write: TypeToken manual",
        "[",
        "  {",
        "    \"custom\": 1",
        "  }",
        "]",
        "===",
        "Read: TypeToken manual",
        "[ClassWithAdapter[3]]",
        "===",
        "Write: Named fields",
        "{",
        "  \"myField\": 2,",
        "  \"notAccessedField\": -1",
        "}",
        "===",
        "Read: Named fields",
        "3",
        "===",
        "Write: SerializedName",
        "{",
        "  \"myField\": 2,",
        "  \"notAccessed\": -1",
        "}",
        "===",
        "Read: SerializedName",
        "3",
        "===",
        "Read: No JDK Unsafe; initial constructor value",
        "-3",
        "===",
        "Read: No JDK Unsafe; custom value",
        "3",
        "===",
        "Write: Enum",
        "\"FIRST\"",
        "===",
        "Read: Enum",
        "SECOND",
        "===",
        "Write: Enum SerializedName",
        "\"one\"",
        "===",
        "Read: Enum SerializedName",
        "SECOND",
        "===",
        "Write: @Expose",
        "{\"i\":0}",
        "===",
        "Write: Version annotations",
        "{\"i1\":0,\"i4\":0}",
        "===",
        "Write: JsonAdapter on fields",
        "{",
        "  \"f\": \"adapter-null\",",
        "  \"f1\": \"adapter-1\",",
        "  \"f2\": \"factory-2\",",
        "  \"f3\": \"serializer-3\",",
        // For f4 only a JsonDeserializer is registered, so serialization falls back to reflection
        "  \"f4\": {",
        "    \"s\": \"4\"",
        "  }",
        "}",
        "===",
        "Read: JsonAdapter on fields",
        // For f3 only a JsonSerializer is registered, so for deserialization value is read as is using reflection
        "ClassWithJsonAdapterAnnotation[f1=adapter-1, f2=factory-2, f3=3, f4=deserializer-4]",
        "===",
        "Read: Generic TypeToken",
        "{t=read-1}",
        "===",
        "Read: Using Generic",
        "{g={t=read-1}}",
        "===",
        "Read: Using Generic TypeToken",
        "{g={t=read-1}}",
        "===",
        ""
      ));
  }

  @Test
  public void testDefaultConstructor() throws Exception {
    runTest("com.example.DefaultConstructorMain", c -> {
      Method m = c.getMethod("runTest");

      if (jarToTest.equals(PROGUARD_RESULT_PATH)) {
        Object result = m.invoke(null);
        assertThat(result).isEqualTo("value");
      } else {
        // R8 performs more aggressive optimizations
        Exception e = assertThrows(InvocationTargetException.class, () -> m.invoke(null));
        assertThat(e).hasCauseThat().hasMessageThat().isEqualTo(
            "Abstract classes can't be instantiated! Adjust the R8 configuration or register an InstanceCreator"
            + " or a TypeAdapter for this type. Class name: com.example.DefaultConstructorMain$TestClass"
            + "\nSee https://github.com/google/gson/blob/master/Troubleshooting.md#r8-abstract-class"
        );
      }
    });
  }

  @Test
  public void testDefaultConstructorNoJdkUnsafe() throws Exception {
    runTest("com.example.DefaultConstructorMain", c -> {
      Method m = c.getMethod("runTestNoJdkUnsafe");

      if (jarToTest.equals(PROGUARD_RESULT_PATH)) {
        Object result = m.invoke(null);
        assertThat(result).isEqualTo("value");
      } else {
        // R8 performs more aggressive optimizations
        Exception e = assertThrows(InvocationTargetException.class, () -> m.invoke(null));
        assertThat(e).hasCauseThat().hasMessageThat().isEqualTo(
            "Unable to create instance of class com.example.DefaultConstructorMain$TestClassNotAbstract;"
            + " usage of JDK Unsafe is disabled. Registering an InstanceCreator or a TypeAdapter for this type,"
            + " adding a no-args constructor, or enabling usage of JDK Unsafe may fix this problem. Or adjust"
            + " your R8 configuration to keep the no-args constructor of the class."
        );
      }
    });
  }
}
